3.2 内存逃逸与堆栈管理

内存逃逸是我们比较容易遇到的一个问题,与程序的性能也息息相关,决定了程序的内存分配效率、内存占用情况。

在之前的章节中我们讲解过了堆栈相关的知识,了解到了内存分配的机制。

本节我们将针对内存逃逸、分析、优化等内容进行详细的讲解。

本节代码存放目录为 lesson8

基础概念

什么是内存逃逸?

在之前章节我们讲过,局部变量等信息都是存储在栈上的,长生命周期的常量等是存储在堆上的。这是内存分配的基本原则。

但是在实际应用中,可能会出现:局部变量被分配到了堆上,换句话说就是:本应分配在栈上的,但实际分配在了堆上,这就是内存逃逸

这是因为当编译器发现某个局部变量在函数返回后仍需要存在或被访问时,编译器就不会把这个变量分配在栈上,而是直接在堆上为它分配内存。

这就叫做内存逃逸。这并不是说一个变量从栈上跑到了堆上,而是说编译器在一开始就决定把这个变量分配到堆上。


内存逃逸的影响

在之前章节我们讲过,栈回收是由系统进行的,堆回收是需要由开发者自行维护的,在Go语言中则是由垃圾回收器GC所维护的。

那么我们举个例子,如果在一个程序中,大量的变量、对象都没有分配在栈上,全都跑到了堆上去了。

结合之前所学习到的堆的特点,那么可能就会出现推存储的数据过多,而回收器由于数据量的问题,不能及时进行回收,最终导致堆上的数据越来越多,那么最终要么是程序崩溃,要么是主机直接就崩溃了。

总的来说,其实就一句话:影响程序的性能

编译器如何决定变量的分配位置?

逃逸分析的概念

编译器使用逃逸分析来决定变量应该分配在栈上还会堆上。

逃逸分析会检查变量的生命周期,如果发现变量在函数返回后仍然还会使用,那么就会将变量分配到堆上。

典型的案例就是:函数返回的是指针变量,那么肯定不能放到栈上的,因为指针还会被引用。


常见的逃逸场景

局部变量的地址被返回

当一个函数返回了局部变量的指针时,该变量必须逃逸到堆上。如下代码所示:

func createPointer() *int {
    x := 10
    return &x
}

在上面的代码中,本身x :=10作为一个局部变量,是肯定会分配在栈上的,但是由于返回了地址,也就是指针,那么意味着这个变量x在函数外部有可能还会被继续使用。

因为返回的是指针,还是可以通过内存地址继续操作x的,所以变量x就只能分配到堆上,这样才能保证函数返回后x还可以继续被操作。


闭包捕获外部变量

当一个闭包捕获了外部变量时,这个外部变量通常需要逃逸到堆上,以确保闭包在函数返回后依然可以访问到这个变量。

如下代码所示:

func createClosure() func() int {
    y := 20
    return func() int {
        return y
    }
}

在上面的代码中,局部变量y被闭包捕获,如果在栈上分配,那么在createClosure返回后y变量就可能访问不到,所以编译器会将闭包函数分配到堆上。

需要注意的是,y并不直接逃逸,而是随着闭包一起被处理,最终是闭包的逃逸,而不是 y本身的逃逸。

内存逃逸检测及分析

Go语言为开发者提供了检测及分析工具,通过工具我们可以快速的对整体代码进行分析优化。

在执行go buildgo run时我们可以增加gcflags参数来输出内存逃逸分析分析。

使用格式为:

go build -gcflags="-m"

或

go run -gcflags="-m" yougogile.go`

我们执行go run -gcflags="-m" lesson8.go查看输出结果如下所示:

# command-line-arguments
./lesson8.go:13:6: can inline createPointer
./lesson8.go:18:6: can inline createClosure
./lesson8.go:20:9: can inline createClosure.func1
./lesson8.go:6:20: inlining call to createPointer
./lesson8.go:7:12: inlining call to fmt.Printf
./lesson8.go:9:20: inlining call to createClosure
./lesson8.go:20:9: can inline main.func1
./lesson8.go:10:25: inlining call to main.func1
./lesson8.go:10:12: inlining call to fmt.Printf
./lesson8.go:7:12: ... argument does not escape
./lesson8.go:7:24: *x escapes to heap
./lesson8.go:9:20: func literal does not escape
./lesson8.go:10:12: ... argument does not escape
./lesson8.go:10:25: ~R0 escapes to heap
./lesson8.go:14:2: moved to heap: x
./lesson8.go:20:9: func literal escapes to heap
x: 10
y: 20

在上面的输出中我们需要关注*x escapes to heapfunc literal escapes to heap,从输出中我们就可以明确的看到x闭包函数发生了逃逸。

与该命令相似的还有:go tool compile -m lesson8.go,执行该命令同样会输出内存逃逸信息。

小结

内存逃逸是我们在开发中经常遇到的一个问题,减少内存逃逸的发生可以提高程序的性能。

关于本节总结如下:

  • 本身应该在栈上的,但是跑到了堆上,这就是内存逃逸

  • 内存逃逸过多会增加GC的压力,影响程序性能

  • 函数如果返回了局部变量指针就会发生内存逃逸

  • 闭包捕获外部变量也会发生闭包整体逃逸

  • 使用gcflags可以进行编译时逃逸分析

results matching ""

    No results matching ""